Passed
Pull Request — master (#136)
by
unknown
02:22 queued 10s
created

Frame.getBuffer   A

Complexity

Conditions 4

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 12
c 0
b 0
f 0
rs 9.95
cc 4
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as Frames from './Frames'
8
import * as ID3Util from './ID3Util'
9
import { isKeyOf } from "./util"
10
11
export class Frame {
12
    identifier: string
13
    private value: unknown
14
    flags: Flags
15
16
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
17
        this.identifier = identifier
18
        this.value = value
19
        this.flags = flags
20
    }
21
22
    static createFromBuffer(
23
        frameBuffer: Buffer,
24
        version: number
25
    ): Frame | null {
26
        const frameHeaderSize = getHeaderSize(version)
27
        // Specification requirement
28
        if (frameBuffer.length < frameHeaderSize + 1) {
29
            return null
30
        }
31
        const frameHeaderBuffer = frameBuffer.subarray(0, frameHeaderSize)
32
        const frameHeader = FrameHeader.createFromBuffer(
33
            frameHeaderBuffer, version
34
        )
35
        if (frameHeader.flags.encryption) {
36
            return null
37
        }
38
39
        const frameBodyOffset = frameHeader.flags.dataLengthIndicator ? 4 : 0
40
        const frameBodyStart = frameHeaderSize + frameBodyOffset
41
        let frameBody = frameBuffer.subarray(frameBodyStart, frameBodyStart + frameHeader.bodySize - frameBodyOffset)
42
        if (frameHeader.flags.unsynchronisation) {
43
            // This method should stay in ID3Util for now because it's also used in the Tag's header which we don't have a class for.
44
            frameBody = ID3Util.processUnsynchronisedBuffer(frameBody)
45
        }
46
47
        const decompressedFrameBody = decompressBody(
48
            frameHeader.flags, frameBuffer, frameHeaderSize, frameBody
49
        )
50
        if (!decompressedFrameBody) {
51
            return null
52
        }
53
        frameBody = decompressedFrameBody
54
55
        const identifier = frameHeader.identifier
56
        let value = null
57
        if (isKeyOf(identifier, Frames.Frames)) {
58
            value = Frames.Frames[identifier].read(frameBody, version)
59
        } else if (identifier.startsWith('T')) {
60
            value = Frames.GENERIC_TEXT.read(frameBody)
61
        } else if (identifier.startsWith('W')) {
62
            value = Frames.GENERIC_URL.read(frameBody)
63
        } else {
64
            return null
65
        }
66
        return new Frame(identifier, value, frameHeader.flags)
67
    }
68
69
    getBuffer() {
70
        if (isKeyOf(this.identifier, Frames.Frames)) {
71
            return Frames.Frames[this.identifier].create(this.value)
72
        }
73
        if (this.identifier.startsWith('T')) {
74
            return Frames.GENERIC_TEXT.create(this.identifier, this.value)
75
        }
76
        if (this.identifier.startsWith('W')) {
77
            return Frames.GENERIC_URL.create(this.identifier, this.value)
78
        }
79
        return null
80
    }
81
82
    getValue() {
83
        return this.value
84
    }
85
}
86
87
function decompressBody(
88
    flags: Flags,
89
    frameBuffer: Buffer,
90
    dataLengthOffset: number,
91
    frameBody: Buffer
92
) {
93
    let dataLength = 0
94
    if (flags.dataLengthIndicator) {
95
        dataLength = frameBuffer.readInt32BE(dataLengthOffset)
96
    }
97
    if (flags.compression) {
98
        return decompressBuffer(frameBody, dataLength)
99
    }
100
    return frameBody
101
}
102
103
104
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
105
    if (buffer.length < 5 || expectedDecompressedLength === undefined) {
106
        return null
107
    }
108
109
    // ID3 spec defines that compression is stored in ZLIB format,
110
    // but doesn't specify if header is present or not.
111
    // ZLIB has a 2-byte header.
112
    // 1. try if header + body decompression
113
    // 2. else try if header is not stored (assume that all content is deflated "body")
114
    // 3. else try if inflation works if the header is omitted (implementation dependent)
115
    const tryDecompress = () => {
116
        try {
117
            return zlib.inflateSync(buffer)
118
        } catch (error) {
119
            try {
120
                return zlib.inflateRawSync(buffer)
121
            } catch (error) {
122
                try {
123
                    return zlib.inflateRawSync(buffer.subarray(2))
124
                } catch (error) {
125
                    return null
126
                }
127
            }
128
        }
129
    }
130
    const decompressed = tryDecompress()
131
    if (!decompressed || decompressed.length !== expectedDecompressedLength) {
132
        return null
133
    }
134
    return decompressed
135
}
136